哈囉,各位好!今天我們要來挑戰實用又有趣的頁面——製作一個互動式的聯絡表單。不管你是剛入門的新手,還是想更深入了解 Nuxt 3 的開發者,這個教學都能讓你學到很多實用的技巧。我們會用到 Nuxt 3、Pinia 和一些基本的表單處理技巧。準備好了嗎?讓我們開始吧!
在 stores 資料夾中修改 api.ts 檔案:
// stores/api.ts
import { defineStore } from 'pinia'
import axios from 'axios'
const baseURL = '<https://two024it-test-app.onrender.com>'
const api = axios.create({
  baseURL
})
export const useApiStore = defineStore('api-store', {
  actions: {
    // 新增回饋
    async createFeedback(data: any) {
      const response = await api.post('/feedbacks', data)
      return response
    }
  }
})
新增 createFeedback 方法,用來將使用者的回饋發送到伺服器。
接下來,我們要在 pages 資料夾中修改 connect.vue 檔案。這個檔案會包含我們的表單和相關邏輯。
<!-- pages/connect.vue -->
<script setup lang="ts">
import { useApiStore } from '~/stores/api'
import { showLoading, hideLoading } from '~/stores/eventBus'
const apiStore = useApiStore()
const contactPerson = ref('')
const phone = ref('')
const email = ref('')
const feedback = ref('')
const source = ref('')
const sourceOption = ['網路搜尋', '社群媒體', '親友介紹', '其他']
// ... 其他程式碼會在後續步驟中添加
</script>
<template>
  <!-- 模板內容會在後續步驟中添加 -->
</template>
在這個檔案中,我們:
在開始實作表單邏輯之前,我們需要先定義清晰的資料結構。這是程式設計中非常重要的一步,它能幫助我們更好地組織和管理數據。
interface FeedbackData {
  contactPerson: string
  phone: string
  email: string
  feedback: string
  source?: string
}
讓我們深入了解這個 interface:
contactPerson、phone、email 和 feedback 都被定義為 string 類型,這意味著它們必須是文字資料。source 後面的 ? 表示這是一個可選欄位。使用者可以填寫,也可以不填寫。使用 interface 的好處:
現在,讓我們深入了解表單的核心邏輯:
// 送出聯絡我們
async function sendContact() {
  try {
    showLoading()
    let data: FeedbackData = {
      contactPerson: contactPerson.value,
      phone: phone.value,
      email: email.value,
      feedback: feedback.value
    }
    // source 有值才加入
    if (source.value) {
      data = { ...data, source: source.value }
    }
    const res = await apiStore.createFeedback(data as any)
    const result = res.data
    if (result && result.status === 'success') {
      alert('感謝您的回饋,我們會盡快處理!')
      
      // 清空表單
      contactPerson.value = ''
      phone.value = ''
      email.value = ''
      feedback.value = ''
      source.value = ''
    } else {
      alert('發生錯誤,請稍後再試!')
    }
  } catch (error) {
    alert('發生錯誤,請稍後再試!')
  } finally {
    hideLoading()
  }
}
// 檢查 email 格式 (blur 事件觸發)
function checkEmail() {
  if (email.value) {
    const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
    if (!emailReg.test(email.value)) {
      alert('請輸入正確的 email 格式!')
      email.value = ''
    }
  }
}
sendContact 函式的詳細說明:
try-catch 結構來處理可能發生的錯誤。showLoading() 顯示載入畫面,提升用戶體驗。data 物件,使用我們之前定義的 FeedbackData interface。source 欄位。apiStore.createFeedback() 發送資料到後端。hideLoading() 來隱藏載入畫面。checkEmail 函式的作用:
blur 事件上,當使用者離開輸入框時觸發。這個 Vue 模板實現了一個功能完整的聯絡表單頁面。讓我們來看看它的三個主要區塊:
<div class="cus-intro lg:hidden">
  使用上遇到困難?<br />希望有更好用的功能?<br />覺得網站很實用?<br />
  把想法都告訴我們吧,<br />我們可以把你的想法化為現實。<br />
  非常歡迎擁有專業知識的夥伴加入我們的 side project ✨
</div>
<div class="cus-intro hidden lg:block">
  使用上遇到困難?希望有更好用的功能?覺得網站很實用?<br />
  把想法都告訴我們吧,我們可以把你的想法化為現實。<br />
  非常歡迎擁有專業知識的夥伴加入我們的 side project ✨
</div>
說明:
lg:hidden 和 hidden lg:block)來控制不同螢幕尺寸下的顯示。html
Copy
<div class="cus-block-padding">
  <h2 class="cus-page-title">填寫表單幫助我們變得更好</h2>
  <div class="cus-col-3">
<!-- 名稱、電話、信箱、內容輸入框 --><!-- 來源選擇(單選按鈕) -->
  </div>
  <button
    class="cus-btn-primary mt-5"
    :disabled="!contactPerson || !phone || !email || !feedback"
    @click="sendContact"
  >
    送出表單
  </button>
</div>
說明:
v-model 指令實現數據的雙向綁定。:disabled 綁定來控制是否可點擊,確保必填欄位都有值。@click="sendContact" 綁定了提交事件。<div class="cus-block-padding">
  <h2 class="cus-page-title">或是你也可以用其他方式聯繫我們</h2>
  <a href="https://profile.2fishs.com/" target="_blank" class="mb-2 flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3">
    <Icon name="ph:link" size="20" />
    <p>profile_web</p>
  </a>
  <a href="mailto:yu13142013@gmail.com" target="_blank" class="flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3">
    <Icon name="ph:envelope-simple-light" size="20" />
    <p>yu13142013@gmail.com</p>
  </a>
</div>
說明:
cus-border、cus-intro 等)來保持整體風格的統一。
pages/connect.vue<!-- pages / connect.vue --> <script setup lang="ts"> import { useApiStore } from '~/stores/api' const apiStore = useApiStore() import { showLoading, hideLoading } from '~/stores/eventBus' const contactPerson = ref('') const phone = ref('') const email = ref('') const feedback = ref('') const source = ref('') // "網路搜尋", "社群媒體", "親友介紹", "其他" const sourceOption = ['網路搜尋', '社群媒體', '親友介紹', '其他'] interface feedbackData { contactPerson: string phone: string email: string feedback: string source?: string // 使 source 屬性成為可選的 } // 送出聯絡我們 async function sendContact() { try { showLoading() let data: feedbackData = { contactPerson: contactPerson.value, phone: phone.value, email: email.value, feedback: feedback.value } // source 有值才加入 if (source.value) { data = { ...data, source: source.value } } const res = await apiStore.createFeedback(data as any) // console.log(res) const result = res.data if (result && result.status === 'success') { alert('感謝您的回饋,我們會盡快處理!') contactPerson.value = '' phone.value = '' email.value = '' feedback.value = '' source.value = '' } else { alert('發生錯誤,請稍後再試!') } } catch (error) { alert('發生錯誤,請稍後再試!') } finally { hideLoading() } } // 檢查 email 格式 (blur 事件觸發) function checkEmail() { if (email.value) { const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ if (!emailReg.test(email.value)) { alert('請輸入正確的 email 格式!') email.value = '' } } } </script> <template> <div class="w-full"> <!-- * content --> <div class="cus-border"> <!-- * introduction --> <div class="cus-intro lg:hidden"> 使用上遇到困難?<br />希望有更好用的功能?<br />覺得網站很實用?<br /> 把想法都告訴我們吧,<br />我們可以把你的想法化為現實。<br /> 非常歡迎擁有專業知識的夥伴加入我們的 side project ✨ </div> <div class="cus-intro hidden lg:block"> 使用上遇到困難?希望有更好用的功能?覺得網站很實用?<br /> 把想法都告訴我們吧,我們可以把你的想法化為現實。<br /> 非常歡迎擁有專業知識的夥伴加入我們的 side project ✨ </div> <hr class="cus-line-row" /> <!-- * feedback info --> <div class="cus-block-padding"> <h2 class="cus-page-title">填寫表單幫助我們變得更好</h2> <div class="cus-col-3"> <div class="cus-col-1"> <label for="contactPerson" class="cus-label" >名稱 <span class="text-red2">*</span></label > <input type="text" class="cus-input" id="contactPerson" v-model="contactPerson" placeholder="請輸入名稱" /> </div> <div class="cus-col-1"> <label for="phone" class="cus-label">電話 <span class="text-red2">*</span></label> <input type="tel" class="cus-input" id="phone" v-model="phone" placeholder="請輸入電話" /> </div> <div class="cus-col-1"> <label for="email" class="cus-label">信箱 <span class="text-red2">*</span></label> <input type="email" class="cus-input" id="email" v-model="email" placeholder="請輸入信箱" @blur="checkEmail" /> </div> <div class="cus-col-1"> <label for="feedback" class="cus-label">內容 <span class="text-red2">*</span></label> <input type="text" class="cus-input" id="feedback" v-model="feedback" placeholder="請輸入內容" /> </div> <div class="cus-col-1"> <label for="source" class="cus-label">從哪裡得知此網站</label> <div class="cus-radio-row"> <label class="cus-label-radio" for="網路搜尋"> <input type="radio" name="source" class="" id="網路搜尋" v-model="source" value="網路搜尋" /> <span></span> 網路搜尋 </label> <label class="cus-label-radio" for="社群媒體"> <input type="radio" name="source" class="" id="社群媒體" v-model="source" value="社群媒體" /> <span></span> 社群媒體 </label> <label for="親友介紹" class="cus-label-radio"> <input type="radio" name="source" class="" id="親友介紹" v-model="source" value="親友介紹" /> <span></span>親友介紹 </label> <label for="其他" class="cus-label-radio"> <input type="radio" name="source" class="" id="其他" v-model="source" value="其他" /> <span></span>其他 </label> </div> </div> </div> <button class="cus-btn-primary mt-5" :disabled="!contactPerson || !phone || !email || !feedback" @click="sendContact" > 送出表單 </button> </div> <hr class="cus-line-row" /> <!-- * contact --> <div class="cus-block-padding"> <h2 class="cus-page-title">或是你也可以用其他方式聯繫我們</h2> <a href="https://profile.2fishs.com/" target="_blank" class="mb-2 flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3" > <Icon name="ph:link" size="20" /> <p>profile_web</p> </a> <a href="mailto:yu13142013@gmail.com" target="_blank" class="flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3" > <Icon name="ph:envelope-simple-light" size="20" /> <p>yu13142013@gmail.com</p> </a> </div> </div> </div> </template> <style scoped></style>
stores/api.ts// stores/api.ts import { defineStore } from 'pinia' import axios from 'axios' const baseURL = 'https://two024it-test-app.onrender.com' const api = axios.create({ baseURL }) export const useApiStore = defineStore('api-store', { actions: { // 取得食物列表 async fetchFoodList() { const response = await api.get('/freshfoods/') return response }, // 新增鮮食計算 async calculateFood(data: any) { const response = await api.post('/foods/calculatefood', data) return response }, // 新增回饋 async createFeedback(data: any) { const response = await api.post('/feedbacks', data) return response } } })
我們已經深入探討了如何使用 Nuxt 3 和 Vue 3 來創建一個功能完整的聯絡表單。從資料結構的定義,到表單邏輯的實現,再到用戶界面的設計,每一步都是前端開發中重要的環節。這個過程不僅讓我們學習了技術細節,更讓我們理解了如何從使用者的角度來思考和設計。
雖然由於時間限制,我們只能實作兩個頁面,但這已經足以讓我們掌握 Nuxt 3 專案的基本架構和開發流程。記住,實踐是學習的最好方式。即使只有兩個頁面,也要盡可能地將所學付諸實踐,這樣才能真正理解和掌握這些概念。
明天,我們將邁出最後一步 —— 將專案部署到雲端!這將是一個將我們的作品展示給全世界的機會。在此之前,別忘了每天都要將你的更新推送到 GitHub。這不僅是一個好習慣,也是確保你的程式碼安全的重要步驟。
最後,記住程式開發是一個持續學習和改進的過程。今天的每一小步,都是邁向成為優秀開發者的重要一步。保持好奇心,勇於嘗試,相信自己的能力。我們在雲端見!
大家有沒有想要更詳細補充的部分呢?歡迎在下方留言分享喔!讓我們一起在 Nuxt3 的世界中探險吧!加油!
(對了,如果你覺得今天的內容對你有幫助,別忘了給個讚支持一下喔!這會是我繼續努力的動力呢~)